const DEFAULT_FTP_PORT = 21;
const ANON_USER = "anonymous";
const ANON_PASS = "anonymous@";

function FtpConnection(aHost, aOpt) {
  aOpt = aOpt || {};
  aOpt.passive = aOpt.passive || false;
  aOpt.port    = aOpt.port    || DEFAULT_FTP_PORT;
  this._transferType = "binary";
  this._passive      = aOpt.passive;
  this._port         = aOpt.port;
  this._socket       = makeClientSocket("inet", aHost, aOpt.port);
  this._iptype       = "IPv4";
}

FtpConnection.prototype = {
  get transferType() {
    return this._transferType;
  },
  set transferType(aInteger) {
    this._transferType = aInteger;
  },
  get passive() {
    return this._passive;
  },
  get port() {
    return this._port;
  },
  get socket() {
    return this._socket;
  },
  get iptype() {
    return this._iptype;
  }
};

function ipv4p(aConn) {
  return (aConn.iptype == "IPv4");
}

function ftpPassivep(aConn) {
  return aConn.passive;
}

function callWithFtpConnection(aURI, aProc, aOpt) {
  let conn = ftpLogin(aURI, aOpt);
  let r = aProc(conn);
  ftpQuit(conn);
  return r;
}

function ftpLogin(aURI, aOpt) {
  function doLogin(conn) {
    function retry() {
      let r0 = getResponse(conn);
      if (stringPrefixP(r0, "120")) {
        //setTimeout(retry, 1000);
        return false;
      }
      let r1 = sendCommand(conn, "USER", aOpt.username);
      if (!stringPrefixP(r1, "3")) return r1;
      let r2 = sendCommand(conn, "PASS", aOpt.password);
      if (!stringPrefixP(r2, "3")) return r2;
      let r3 = sendCommand(conn, "ACCT", aOpt.account);
      return r3;
    }
    return retry();
  }

  let uri = IOService.newURI(aURI, null, null);
  aOpt = aOpt || {};
  aOpt.account  = aOpt.account || "";
  aOpt.username = uri.username || aOpt.username || ANON_USER;
  aOpt.password = uri.password || aOpt.password || (aOpt.username == ANON_USER ? ANON_PASS : "");
  aOpt.port     = (uri.port != -1) ? uri.port : (aOpt.port || DEFAULT_FTP_PORT);
  aOpt.passive  = true;//aOpt.passive || false;

  let conn = new FtpConnection(uri.asciiHost, aOpt);
  let res = doLogin(conn);
  if (!stringPrefixP(res, "2")) throw new Error("ftpError" + res);
  return conn;
}

function ftpQuit(aConn) {
  simpleCommand(aConn, "QUIT");
  if (aConn.socket) {
    socketClose(aConn.socket);
    aConn._socket = null;
  }
}

function ftpChdir(aConn, aDirName) {
  if (aDirName == "..") {
    return simpleCommand(aConn, "CDUP");
  }
  return simpleCommand(aConn, "CWD", aDirName);
}

function ftpRemove(aConn, aPath) {
  return simpleCommand(aConn, "DELE", aPath);
}

function ftpHelp(aConn, aOpt) {
  return simpleCommand(aConn, "HELP", aOpt);
}

function _internalParse257(res) {
  let m = res.match(/^257 \"((?:[^\"]|\"\")+)\"/);
  if (!m) throw new Error("ftpError" + res);
  return m[1].replace(/\"\"/, "\"", "g");
}

function ftpMkdir(aConn, aDirName) {
  return _internalParse257(simpleCommand(aConn, "MKD", aDirName));
}

function ftpCurrentDirectory(aConn) {
  return _internalParse257(simpleCommand(aConn, "PWD"));
}

function ftpSite(aConn, aArg) {
  return simpleCommand(aConn, "SITE", aArg);
}

function ftpRmdir(aConn, aDirName) {
  return simpleCommand(aConn, "RMD", aDirName);
}

function ftpStat(aConn, aOpt) {
  return simpleCommand(aConn, "STAT", aOpt);
}

function ftpSystem(aConn) {
  return simpleCommand(aConn, "SYST").substring(4);
}

function ftpSize(aConn, aPath) {
  return parseInt(simpleCommand(aConn, "SIZE", aPath).substring(4), 10);
}

function ftpMdtm(aConn, aPath) {
  return simpleCommand(aConn, "MDTM", aPath);
}

//TODO
function ftpMtime(aConn, aPath, aLocalTime) {
  aLocalTime = !!aLocalTime;
  let res = ftpMdtm(aConn, aPath);
  let m = res.match(/(\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)/);
  if (!m) throw new Error("ftpError" + res);
  let [year, month, day, hour, min, sec] = m;
  return aLocalTime ?
    new Date(parseInt(year, 10),
             parseInt(month, 10),
             parseInt(day, 10),
             parseInt(hour, 10),
             parseInt(min, 10),
             parseInt(sec, 10)) :
    new Date(Date.UTC(parseInt(year, 10),
                      parseInt(month, 10),
                      parseInt(day, 10),
                      parseInt(hour, 10),
                      parseInt(min, 10),
                      parseInt(sec, 10)));
}

function ftpNoop(aConn) {
  return simpleCommand(aConn, "NOOP");
}

function _internalMakeLister(aCmd, aConn, aOpt) {
  return reqRecv(aConn,
                 function () {
                   let r = aOpt ? sendCommand(aConn, aCmd, aOpt) : sendCommand(aConn, aCmd);
                   return r;
                 },
                 openOutputString(),
                 function (aOutputStream) {
                   return getOutputString(aOutputStream).replace(/\r/g, "").replace(/\n/g, "\r\n").split(/\r\n/);
                 },
                 "ascii");
}

function ftpList(aConn, aOpt) {
  return _internalMakeLister("LIST", aConn, aOpt);
}

function ftpNameList(aConn, aOpt) {
  return _internalMakeLister("NLST", aConn, aOpt);
}

let ftpLs = ftpNameList;

function ftpGet(aConn, aPath, aOpt) {
  if (!aOpt) aOpt = {};
  aOpt.sink    = aOpt.sink    || openOutputString();
  aOpt.flusher = aOpt.flusher || getOutputString;
  return reqRecv(aConn,
                 function() {
                   return sendCommand(aConn, "RETR", aPath);
                 },
                 aOpt.sink,
                 aOpt.flusher,
                 "binary");
}

function ftpPut(aConn, aFromFile, aToFile) {
  aToFile = aToFile || sysBasename(aFromFile);
  return callWithInputFile(aFromFile, function(inp) {
                             return reqSend(aConn,
                                            function() {
                                              return sendCommand(aConn, "STOR", aToFile);
                                            },
                                            inp)[0];
                           });
}

function ftpPutFromStream(aConn, aInputStream, aToFile) {
  return reqSend(aConn,
                 function() {
                   return sendCommand(aConn, "STOR", aToFile);
                 },
                 aInputStream)[0];
}

function ftpPutUnique(aConn, aFromFile) {
  return callWithInputFile(aFromFile, function(inp) {
                             return reqSend(aConn,
                                            function() {
                                               return sendCommand(aConn, "STOU");
                                            },
                                            inp);
                           });
}

function ftpPutUniqueFromStream(aConn, aInputStream) {
  return reqSend(aConn,
                 function() {
                   return sendCommand(aConn, "STOU");
                 },
                 aInputStream);
}

function ftpRename(aConn, aFrom, aTo) {
  let res1 = sendCommand(aConn, "RNFR", aFrom);
  if (!stringPrefixP(res1, "3")) throw new Error("ftpError" + res1);
  let res2 = sendCommand(aConn, "RNTO", aTo);
  if (!stringPrefixP(res2, "2")) throw new Error("ftpError" + res2);
  return res2;
}

function _callWithDataConnection(aConn, aProc) {
  function parse227(res) {
    if (!stringPrefixP(res,"227")) throw new Error("ftpError" + res);
    let m = res.match(/\((\d+),(\d+),(\d+),(\d+),(\d+),(\d+)\)/);
    if (!m) throw new Error("ftpError" + res);
    let [, h1, h2, h3, h4, p1, p2] = m;
    return makeClientSocket("inet",
                            [h1, h2, h3, h4].join("."),
                            256 * parseInt(p1, 10) + parseInt(p2, 10));
  }
  function parse229(res) {
    //TODO implement parse229 for ipv6;
  }
  let result = null;;
  if (ftpPassivep(aConn)) {
    let dcsock = null;
    if (ipv4p(aConn)) {
      dcsock = parse227(simpleCommand(aConn, "PASV"));
    } else {
      dcsock = parse229(simpleCommand(aConn, "EPSV"));
    }
    result = aProc(dcsock);
    socketClose(dcsock);
  } else {
    return result;
    //TODO implement socketAccept.
    let asock = makeServerSocket("inet", 0);
    let psname = asock.address.name;
    if (ipv4p(aConn)) {
      let m = psname.match(/(\d+)\.(\d+)\.(\d+)\.(\d+):(\d+)/);
      if (!m) throw new Error("ftpError" + psname);
      let [, a, b, c, d, p] = m;
      let q = Math.floor(parseInt(p, 10) / 256);
      let r = parseInt(p, 10) % 256;
      simpleCommand(aConn, "PORT", [a, b, c, d, q, r].join(","));
      let ds = socketAccept(asock);
      result = aProc(ds);
      socketClose(ds);
      socketClose(asock);
    }
  }
  return result;
}

function sendCommand(aConn, aCmd) {
  let args = arguments;
  let msg = callWithOutputString(function(o) {
                                   display(aCmd, o);
                                   for (let i = 2; i < args.length; i++) {
                                     display(" ", o);
                                     display(args[i], o);
                                   }
                                 });
  let outp = socketOutputStream(aConn.socket);
  display(   msg, outp);
  display("\r\n", outp);
  flush(outp);
  return getResponse(aConn);
}

function simpleCommand(aConn, aCmd) {
  let res = sendCommand.apply(null, arguments);
  if (!stringPrefixP(res, "2")) throw new Error("ftpError" + res);
  return res;
}

function getResponse(aConn) {
  let inp = socketInputStream(aConn.socket);
  let l = readLineSync(inp);
  let m = l.match(/^(\d\d\d)-/);
  if (!m) return l;
  let code = m[1];
  let rs = [];
  let line;
  do {
    line = readLineSync(inp);
    rs.push(line);
  } while (!(stringPrefixP(line, code) && line[3] == " "))
  let res = rs.join("");
  if (/^[45]/.test(res)) throw new Error("ftpError" + res);
  return res;
}

function ftpSetType(aConn, aTransferType) {
  aTransferType = aTransferType || aConn.transferType;
  let t = "";
  switch(aTransferType) {
  case "ascii":
    t = "A";
    break;
  case "binary": //FALLTHROUGH
  case "image":
    t = "I";
    break;
  default:
    throw new Error("Invalid transfer type:" + aTransferType);
  }
  aConn.transferType = aTransferType;
  return simpleCommand(aConn, "TYPE", t);
}

function reqRecv(aConn, aCmdProc, aSink, aFlusher, aTransferType) {
  if (aTransferType) ftpSetType(aConn, aTransferType);
  return _callWithDataConnection(aConn, function(dcsock) {
      let inp = socketInputStream(dcsock, {sink: aSink});
      let r1 = aCmdProc();
      if (!stringPrefixP(r1, "1")) throw new Error("ftpError" + r1);
      waitFinish(inp);
      let res = aFlusher(aSink);
      closeOutputStream(aSink);
      let r2 = getResponse(aConn);
      if (!stringPrefixP(r2, "2")) throw new Error("ftpError" + r2);
      return res;
    });
}

function reqSend(aConn, aCmdProc, aInputStream) {
  function sendData() {
    return _callWithDataConnection(aConn, function(dcsock) {
      socketInputStream(dcsock); //force connect.
      let res = aCmdProc();
      let m0 = res.match(/^1\d\d FILE: (.+)$/);
      if (m0) {
        let dstPath = m0[1];
        copyStream(aInputStream, socketOutputStream(dcsock));
        return dstPath;
      }
      let m1 = /^1/.test(res);
      if (m1) {
        copyStream(aInputStream, socketOutputStream(dcsock));
        return false;
      }
      throw new Error("ftpError" + res);
    });
  }
  function retrieveResponse(res, dstPath) {
    if (/^2/.test(res)) return [res, dstPath];
    if (/^1/.test(res)) return retrieveResponse(getResponse(aConn), dstPath);
    throw new Error("ftpError" + res);
  }
  ftpSetType(aConn);
  let dstPath = sendData();
  return retrieveResponse(getResponse(aConn), dstPath);
}
